//
// Created by Göksu Güvendiren on 2019-05-14.
//

#include "Scene.hpp"


void Scene::buildBVH() {
    printf(" - Generating BVH...\n\n");
    this->bvh = new BVHAccel(objects, 1, BVHAccel::SplitMethod::NAIVE);
}

Intersection Scene::intersect(const Ray &ray) const
{
    return this->bvh->Intersect(ray);
}

void Scene::sampleLight(Intersection &pos, float &pdf) const
{
    float emit_area_sum = 0;
    for (uint32_t k = 0; k < objects.size(); ++k) {
        if (objects[k]->hasEmit()){
            emit_area_sum += objects[k]->getArea();
        }
    }
    float p = get_random_float() * emit_area_sum;
    emit_area_sum = 0;
    for (uint32_t k = 0; k < objects.size(); ++k) {
        if (objects[k]->hasEmit()){
            emit_area_sum += objects[k]->getArea();
            if (p <= emit_area_sum){
                objects[k]->Sample(pos, pdf);
                break;
            }
        }
    }
}

bool Scene::trace(
        const Ray &ray,
        const std::vector<Object*> &objects,
        float &tNear, uint32_t &index, Object **hitObject)
{
    *hitObject = nullptr;
    for (uint32_t k = 0; k < objects.size(); ++k) {
        float tNearK = kInfinity;
        uint32_t indexK;
        Vector2f uvK;
        if (objects[k]->intersect(ray, tNearK, indexK) && tNearK < tNear) {
            *hitObject = objects[k];
            tNear = tNearK;
            index = indexK;
        }
    }


    return (*hitObject != nullptr);
}

// Implementation of Path Tracing
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
    Intersection inter = intersect(ray);

    if (!inter.happened)
    {
        return this->backgroundColor;
    }

    Vector3f p = inter.coords;
    Vector3f wo = ray.direction;
    Vector3f N = inter.normal;

    Intersection light_inter;
    float pdf_light;
    sampleLight(light_inter, pdf_light);

    Vector3f L_dir = 0;

    Vector3f x = light_inter.coords;
    Vector3f NN = light_inter.normal;
    Vector3f ws = (x - p).normalized();
    // Shoot a ray from p to x
    Vector3f p_deviation = (dotProduct(ray.direction, N) < 0) ?
                p + N * EPSILON:
                p - N * EPSILON;
    Ray p_to_x_ray = Ray(p_deviation, ws);
    // If the ray is not blocked in the middle
    float p_to_x_dis = (x - p).norm();
    Intersection p_to_x_inter = intersect(p_to_x_ray);

    if (abs(p_to_x_dis - p_to_x_inter.distance) < 0.01)
    {
        L_dir = light_inter.emit * inter.m->eval(wo, ws, N)
                    * dotProduct(ws, N)
                    * dotProduct(-ws, NN)
                    / pow(p_to_x_dis, 2) / pdf_light;
    }

    // Test Russian Roulette with probability RussianRoulette
    if (get_random_float() > RussianRoulette)
    {
        return inter.m->getEmission() + L_dir;
    }

    Vector3f L_indir = 0;

    Vector3f wi = inter.m->sample(wo, N);
    Ray bounce_ray = Ray(p, wi);
    Intersection bounce_inter = intersect(bounce_ray);
    if (bounce_inter.happened && !bounce_inter.m->hasEmission())
    {
        float pdf = inter.m->pdf(wo, wi, N);
        if (pdf > EPSILON)
        {
            L_indir = castRay(bounce_ray, depth + 1) * inter.m->eval(wo, wi, N)
                    * dotProduct(wi, N) / pdf / RussianRoulette;
        }
    }

    return inter.m->getEmission() + L_dir + L_indir;
}